package com.myproject.mall.common.util;

import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.myproject.mall.common.constants.RedisConstants;
import com.myproject.mall.common.exception.GlobalException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Component
public class CacheUtil {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public <T> void setString(String key, T value, Long time, TimeUnit timeUnit) {
        RedisData<T> redisData = makeRedisData(key, value, time, timeUnit);
        stringRedisTemplate.opsForValue().set(key, JsonUtil.toJson(redisData));
    }

    public <T> void updateCache(String key, T value, Long time, TimeUnit timeUnit) {
        stringRedisTemplate.delete(key);
        setString(key, value, time, timeUnit);
    }

    public void setEmpty(String key, Long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, "", time, timeUnit);
    }

    public <T> void updateStringCache(String key, T value, Long time, TimeUnit timeUnit) {
        stringRedisTemplate.delete(key);
        setString(key, value, time, timeUnit);
    }

    public void delete(String key) {
        stringRedisTemplate.delete(key);
    }

    public <T, ID> T getWithPassThrough(String key, TypeReference<T> clazz, Function<ID, T> dbFallback, ID id, Long time, TimeUnit timeUnit) {
        String json = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank( json)) {
            return JsonUtil.toObject(json, clazz);
        }
        if( json != null) return null;

        T data = dbFallback.apply(id);
        if(data == null) {
            stringRedisTemplate.opsForValue().set(key, "", time, timeUnit);
            return null;
        }

        this.setString(key, data, time, timeUnit);
        return data;
    }

    public <T, ID> T getWithLoginExpire(String key, TypeReference<T> clazz, Function<ID, T> dbFallback, ID id, Long time, TimeUnit timeUnit) {
        String json = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isBlank( json)) {
            return null;
        }
//        命中
        RedisData<T> redisData = JsonUtil.toObject(json, new TypeReference<RedisData<T>>() {});
        T data = redisData.getData();
        LocalDateTime expireTime = redisData.getExpireTime();
        if(expireTime.isAfter(LocalDateTime.now())) {
            return data;
        }
        String lockKey = RedisConstants.LOCK_PREFIX + id;
        boolean isLock = tryLock(lockKey);
        if(isLock) {
            try {
                CACHE_REBUILD_EXECUTOR.submit(()->{
                    T dbData = dbFallback.apply(id);
                    setString(key, dbData, time, timeUnit);
                });
            }
            catch (Exception e) {
                throw new GlobalException(e.getMessage());
            }
            finally {
                unlock(lockKey);
            }
        }
        return data;
    }

    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "lock", 10L, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(flag);
    }
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

    private <T> RedisData<T> makeRedisData(String key, T value, Long time, TimeUnit timeUnit) {
        RedisData<T> redisData = new RedisData<>();
        redisData.setData(value);
        redisData.setExpireTime(makeLogicExpire(time, timeUnit));
        return redisData;
    }

    private LocalDateTime makeLogicExpire(Long time, TimeUnit timeUnit) {
        return LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time));
    }



}
